package com.easyaop.api.support;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandleInfo;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Objects;
import java.util.Queue;
import java.util.Set;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;

import com.easyaop.commons.ASMOpcodes;
import com.easyaop.commons.Constants;
import com.easyaop.commons.asm.ClassEmitter;
import com.easyaop.commons.asm.CodeEmitter;
import com.easyaop.commons.asm.Local;
import com.easyaop.commons.asm.Signature;
import com.easyaop.commons.utils.TypeUtils;

final class ClassAdapter extends ClassVisitor {
    private final Map<String, Type> virtualProtectedField;

    private final Map<String, Set<Type>> staticProtectedField;

    private final Map<Signature, Type> virtualProtectedMethod;

    private final Map<Signature, Set<Type>> staticProtectedMethod;

    private final Set<FieldData> fieldCache = new HashSet<>();

    private final Set<MethodData> methodCache = new HashSet<>();

    private final Set<Signature> methodMHCache = new HashSet<>();

    private final Set<String> setFieldMHCache = new HashSet<>();

    private final ClassEmitter realClassEmitter;

    private final MethodHandleInfo implInfo;

    private final String targetMethodName;

    private final MethodType targetMethodType;

    private final Type outerClassType;

    private final int majorVersion;

    public ClassAdapter(ClassEmitter ce, MethodHandleInfo implInfo,
                        String targetMethodName, MethodType targetMethodType, Class<?> targetClass, int majorVersion) {
        super(ASMOpcodes.ASM_API, new ClassWriter(0));
        this.implInfo = implInfo;
        this.targetMethodName = targetMethodName;
        this.targetMethodType = targetMethodType;
        this.realClassEmitter = ce;
        this.outerClassType = Type.getType(targetClass);
        this.majorVersion = majorVersion;
        this.staticProtectedField = getStaticProtectedField(targetClass);
        this.virtualProtectedField = getVirtualProtectedField(targetClass);
        this.staticProtectedMethod = getStaticProtectedMethod(targetClass);
        this.virtualProtectedMethod = getVirtualProtectedMethod(targetClass);
    }

    private static Map<String, Set<Type>> getStaticProtectedField(Class<?> targetClass) {
        Map<String, Set<Type>> map = new HashMap<>();
        Set<Type> set = new HashSet<>();
        set.add(Type.getType(targetClass));
        getStaticProtectedField(targetClass, map, set);
        return map;
    }

    private static void getStaticProtectedField(Class<?> targetClass, Map<String, Set<Type>> map, Set<Type> set) {
        Class<?> superType = targetClass.getSuperclass();
        if (targetClass == Object.class || superType == null || Object.class.equals(superType)) {
            return;
        }
        set.add(Type.getType(superType));
        if (!superType.getPackage().equals(targetClass.getPackage())) {
            for (Field field : superType.getDeclaredFields()) {
                if (TypeUtils.isProtected(field.getModifiers()) && TypeUtils.isStatic(field.getModifiers())) {
                    map.compute(field.getName(), (key, value) -> {
                        Set<Type> types = value;
                        if (types == null) {
                            types = new HashSet<>();
                        }
                        types.addAll(set);
                        return types;
                    });
                }
            }
        }
        getStaticProtectedField(superType, map, set);
    }

    private static Map<String, Type> getVirtualProtectedField(Class<?> targetClass) {
        Map<String, Type> map = new HashMap<>();
        Queue<Class<?>> queue = new LinkedList<>();
        getVirtualProtectedField(targetClass, map, queue);
        return map;
    }

    private static void getVirtualProtectedField(Class<?> targetClass, Map<String, Type> map, Queue<Class<?>> queue) {
        Class<?> superType = targetClass.getSuperclass();
        if (targetClass == Object.class || superType == null || Object.class.equals(superType)) {
            return;
        }
        queue.add(superType);
        if (!superType.getPackage().equals(targetClass.getPackage())) {
            for (Field field : superType.getDeclaredFields()) {
                if (TypeUtils.isProtected(field.getModifiers()) && !TypeUtils.isStatic(field.getModifiers())) {
                    map.put(field.getName(), Type.getType(queue.peek()));
                }
            }
        }
        getVirtualProtectedField(superType, map, queue);
    }

    private static Map<Signature, Type> getVirtualProtectedMethod(Class<?> targetClass) {
        Map<Signature, Type> map = new HashMap<>();
        getVirtualProtectedMethod(targetClass, map, new LinkedList<>());
        return map;
    }

    private static void getVirtualProtectedMethod(Class<?> targetClass, Map<Signature, Type> map, Queue<Type> queue) {
        Class<?> superType = targetClass.getSuperclass();
        if (targetClass == Object.class || superType == null || Object.class.equals(superType)) {
            return;
        }
        queue.add(Type.getType(superType));
        if (!superType.getPackage().equals(targetClass.getPackage())) {
            for (Method method : superType.getDeclaredMethods()) {
                Signature sig = null;
                if (TypeUtils.isProtected(method.getModifiers()) && !TypeUtils.isStatic(method.getModifiers())) {
                    sig = new Signature(method);
                    map.computeIfAbsent(sig, key -> queue.peek());
                }
            }
        }
        getVirtualProtectedMethod(superType, map, queue);
    }

    private static Map<Signature, Set<Type>> getStaticProtectedMethod(Class<?> targetClass) {
        Map<Signature, Set<Type>> map = new HashMap<>();
        Set<Type> set = new HashSet<>();
        set.add(Type.getType(targetClass));
        getStaticProtectedMethod(targetClass, map, set);
        return map;
    }

    private static void getStaticProtectedMethod(Class<?> targetClass, Map<Signature, Set<Type>> map, Set<Type> set) {
        Class<?> superType = targetClass.getSuperclass();
        if (targetClass == Object.class || superType == null || Object.class.equals(superType)) {
            return;
        }
        set.add(Type.getType(superType));
        if (!superType.getPackage().equals(targetClass.getPackage())) {
            for (Method method : superType.getDeclaredMethods()) {
                Signature sig = null;
                if (TypeUtils.isProtected(method.getModifiers()) && TypeUtils.isStatic(method.getModifiers())) {
                    sig = new Signature(method);
                    map.compute(sig, (key, value) -> {
                        Set<Type> types = value;
                        if (types == null) {
                            types = new HashSet<>();
                        }
                        types.addAll(set);
                        return types;
                    });
                }
            }
        }
        getStaticProtectedMethod(superType, map, set);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,
                                     String[] exceptions) {
        if (implInfo.getName().equals(name) && implInfo.getMethodType().toMethodDescriptorString().equals(descriptor)) {
            int newAccess = access | ASMOpcodes.ACC_STATIC;
            CodeEmitter codeEmitter = realClassEmitter.begin_method(newAccess, targetMethodName,
                    targetMethodType,
                    TypeUtils.fromInternalNames(exceptions));
            return new MethodDelegate(codeEmitter.getClassEmitter().getClassType().getInternalName(), newAccess,
                    targetMethodName, targetMethodType.toMethodDescriptorString(),
                    codeEmitter, access, this);
        }
        return super.visitMethod(access, name, descriptor, signature,
                exceptions);
    }

    public Type getOuterClassType() {
        return outerClassType;
    }

    public int getMajorVersion() {
        return majorVersion;
    }

    public boolean existProtectedFieldInvoke(int opcode, Type owner, String name) {
        if (FieldData.isStaticField(opcode)) {
            return staticProtectedField.containsKey(name) && staticProtectedField.get(name).contains(owner);
        }
        return virtualProtectedField.containsKey(name) && (outerClassType.equals(owner) || virtualProtectedField.get(name).equals(owner));
    }

    public boolean existProtectedMethodInvoke(int opcode, Type owner, Signature methodSig) {
        if (MethodData.isStaticMethod(opcode)) {
            return staticProtectedMethod.containsKey(methodSig) && staticProtectedMethod.get(methodSig).contains(owner);
        }
        return virtualProtectedMethod.containsKey(methodSig) && (outerClassType.equals(owner) || virtualProtectedMethod.get(methodSig).equals(owner));
    }

    public boolean existMethodMH(MethodData methodData) {
        return methodMHCache.contains(methodData.getMhSig());
    }

    public boolean addMethodMH(MethodData methodData) {
        return methodMHCache.add(methodData.getMhSig());
    }

    public boolean existMethodData(MethodData methodData) {
        return methodCache.contains(methodData);
    }

    public boolean addMethodData(MethodData methodData) {
        return methodCache.add(methodData);
    }

    public boolean existFieldData(FieldData fieldData) {
        return fieldCache.contains(fieldData);
    }

    public boolean addFieldData(FieldData fieldData) {
        return fieldCache.add(fieldData);
    }

    public boolean existFieldMH(FieldData fieldData) {
        return setFieldMHCache.contains(fieldData.getMhMethodInvokeName());
    }

    public boolean addFieldMH(FieldData fieldData) {
        return setFieldMHCache.add(fieldData.getMhMethodInvokeName());
    }

    static class MethodDelegate extends AnalyzerAdapter {
        private static final String DEFAULT_OUTER_THIS = "%s$this";

        private static final String MH_FIELD_PRE = "EASY_AOP$$field$";

        private static final String MH_METHOD_PRE = "EASY_AOP$$";

        private static final int OP_FIELD = ASMOpcodes.ACC_PRIVATE | ASMOpcodes.ACC_STATIC | ASMOpcodes.ACC_FINAL
                | ASMOpcodes.ACC_FINAL | ASMOpcodes.ACC_SYNTHETIC;

        private static final int OP_BRIDGE =
                ASMOpcodes.ACC_PRIVATE | ASMOpcodes.ACC_STATIC | ASMOpcodes.ACC_FINAL | ASMOpcodes.ACC_SYNTHETIC;

        private static final int OP_MH_FACTORY = ASMOpcodes.ACC_PRIVATE | ASMOpcodes.ACC_STATIC | ASMOpcodes.ACC_FINAL
                | ASMOpcodes.ACC_FINAL | ASMOpcodes.ACC_VARARGS | ASMOpcodes.ACC_SYNTHETIC;

        private final ClassAdapter ca;

        private final int oldAccess;

        public MethodDelegate(String owner, int access, String name, String descriptor,
                              MethodVisitor methodVisitor, int oldAccess, ClassAdapter ca) {
            super(ASMOpcodes.ASM_API, owner, access, name, descriptor, methodVisitor);
            this.oldAccess = oldAccess;
            this.ca = ca;
        }

        @Override
        public void visitFieldInsn(int opcode, String owner, String name, String descriptor) {
            Type ownerType = Type.getObjectType(owner);
            if (ca.getMajorVersion() <= 8 || !ca.existProtectedFieldInvoke(opcode, ownerType, name)) {
                super.visitFieldInsn(opcode, owner, name, descriptor);
                return;
            }
            FieldData fieldData = createFieldData(opcode, ownerType, name,
                    Type.getType(descriptor));
            Signature mhSig = createFieldSig(fieldData);
            CodeEmitter codeEmitter = (CodeEmitter) mv;
            ClassEmitter ce = codeEmitter.getClassEmitter();
            if (!ca.existFieldData(fieldData)) {
                generateOuterFieldBridge(ce, fieldData, mhSig);
                ca.addFieldData(fieldData);
            }
            super.visitMethodInsn(ASMOpcodes.INVOKESTATIC, ce.getClassType().getInternalName(),
                    mhSig.getName(), mhSig.getDescriptor(), false);
        }

        private Signature createFieldSig(FieldData fieldData) {
            String fieldBridgeName = createFieldBridgeName(fieldData);

            Type methodType = null;
            switch (fieldData.getOpcode()) {
                case ASMOpcodes.PUTFIELD:
                    methodType = Type.getMethodType(Type.VOID_TYPE, ca.getOuterClassType(), fieldData.getType());
                    break;
                case ASMOpcodes.GETFIELD:
                    methodType = Type.getMethodType(fieldData.getType(), ca.getOuterClassType());
                    break;
                case ASMOpcodes.PUTSTATIC:
                    methodType = Type.getMethodType(Type.VOID_TYPE, fieldData.getType());
                    break;
                case ASMOpcodes.GETSTATIC:
                    methodType = Type.getMethodType(fieldData.getType());
                    break;
                default:
                    throw new IllegalArgumentException("opcode: " + fieldData.getOpcode());
            }
            return new Signature(fieldBridgeName, methodType.getDescriptor());
        }

        private String createFieldBridgeName(FieldData fieldData) {
            String bridgeName = null;
            if (fieldData.isStaticField()) {
                bridgeName = fieldData.getOwner().getInternalName().replace('/', '_');
            } else {
                bridgeName = fieldData.getOwner().getInternalName();
                bridgeName = bridgeName.substring(bridgeName.lastIndexOf('/') + 1);
            }
            return  String.format("%s$%s$%s", bridgeName, fieldData.getName(), fieldData.getMhMethodInvokeName());
        }

        private void generateOuterFieldBridge(ClassEmitter ce,
                                                  FieldData fieldData,
                                                  Signature mhSig) {
            FieldSignature fieldSig =
                    new FieldSignature("MHF_" + mhSig.getName(),
                            MethodHandle.class);
            ce.declare_field(OP_FIELD,
                    fieldSig.getName(),
                    fieldSig.getType(), null);

            generateBridgeMethod(ce, fieldSig, mhSig);
            setFieldMHValue(ce, fieldSig, fieldData);
        }

        private FieldData createFieldData(int opcode, Type owner, String name,
                                          Type type) {
            return FieldData.of(opcode, owner, name, type);
        }

        private void setFieldMHValue(ClassEmitter ce, FieldSignature fieldSig,
                                     FieldData fieldData) {
            CodeEmitter staticBlock = ce.getStaticBlock();
            staticBlock.push(fieldData.getName());
            staticBlock.push(fieldData.getType());

            String fieldMHName = MH_FIELD_PRE + fieldData.getMhMethodInvokeName();
            if (!ca.existFieldMH(fieldData)) {
                fieldMethodHandle(ce, fieldMHName, fieldData);
                ca.addFieldMH(fieldData);
            }
            staticBlock.invoke_static_this(fieldMHName, MethodType.methodType(MethodHandle.class, String.class,
                    Class.class));
            staticBlock.putfield(fieldSig.getName());
        }

        private void fieldMethodHandle(ClassEmitter ce, String methodName,
                                       FieldData fieldData) {
            CodeEmitter fieldMH =
                    ce.begin_method(OP_MH_FACTORY,
                            methodName, MethodType.methodType(MethodHandle.class, String.class,
                                    Class.class));
            Label l0 = fieldMH.make_label();
            Label l1 = fieldMH.make_label();
            Label l2 = fieldMH.make_label();

            fieldMH.catch_exception(l0, l1, l2, NoSuchFieldException.class);
            fieldMH.catch_exception(l0, l1, l2, IllegalAccessException.class);

            fieldMH.mark(l0);
            fieldMH.push(ca.getOuterClassType());
            fieldMH.invoke_static(MethodHandles.class, "lookup",
                    MethodType.methodType(MethodHandles.Lookup.class));
            fieldMH.invoke_static(MethodHandles.class, "privateLookupIn",
                    MethodType.methodType(MethodHandles.Lookup.class, Class.class, MethodHandles.Lookup.class));
            fieldMH.push(ca.getOuterClassType());
            fieldMH.load_args();
            fieldMH.invoke_virtual(MethodHandles.Lookup.class, fieldData.getMhMethodInvokeName(),
                    MethodType.methodType(MethodHandle.class,
                            Class.class, String.class, Class.class));
            fieldMH.mark(l1);
            fieldMH.return_value();

            // ex handle
            fieldMH.mark(l2);

            Local exLocal = fieldMH.store_local(ReflectiveOperationException.class);

            Label l4 = fieldMH.make_label();

            fieldMH.mark(l4);

            fieldMH.new_instance(RuntimeException.class);
            fieldMH.dup();
            fieldMH.load_local(exLocal);

            fieldMH.invoke_constructor(RuntimeException.class, Throwable.class);
            fieldMH.athrow();
            fieldMH.end_method();
        }

        @Override
        public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
            Type ownerType = Type.getObjectType(owner);
            Signature methodSig = new Signature(name, descriptor);
            if (ca.getMajorVersion() > 8 && (matchSpecialInvoke(opcode, ownerType, name, descriptor)
                    || ca.existProtectedMethodInvoke(opcode, ownerType, methodSig))) {
                MethodData methodData = MethodData.of(opcode, ownerType, methodSig);
                Signature bridgeSig = createMethodBridgeSig(methodSig, methodData);
                CodeEmitter codeEmitter = (CodeEmitter) mv;
                ClassEmitter ce = codeEmitter.getClassEmitter();
                if (!ca.existMethodData(methodData)) {
                    generateOuterMethodBridge(ce, methodData, bridgeSig);
                    ca.addMethodData(methodData);
                }
                super.visitMethodInsn(ASMOpcodes.INVOKESTATIC, ce.getClassType().getInternalName(),
                        bridgeSig.getName(), bridgeSig.getDescriptor(), false);
                return;
            }
            super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
        }

        private void generateOuterMethodBridge(ClassEmitter ce, MethodData methodData, Signature bridgeSig) {
            FieldSignature fieldSig =
                    new FieldSignature("MH_" + bridgeSig.getName(), MethodHandle.class);
            ce.declare_field(OP_FIELD,
                    fieldSig.getName(),
                    fieldSig.getType(), null);

            generateBridgeMethod(ce, fieldSig, bridgeSig);
            setMethodMHValue(ce, fieldSig, methodData);
        }

        private boolean matchSpecialInvoke(int opcode, Type owner, String name, String descriptor) {
            if (ASMOpcodes.INVOKESPECIAL != opcode || Constants.NAME_CTOR.equals(name)) {
                return false;
            }
            pop(descriptor);
            Object value = peek();
            boolean result = ca.getOuterClassType().getInternalName().equals(value);
            pushArgumentTypes(descriptor);
            return result;
        }

        @Override
        public AnnotationVisitor visitParameterAnnotation(int parameter, String descriptor, boolean visible) {
            return null;
        }

        @Override
        public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
            return null;
        }

        private Signature createMethodBridgeSig(Signature methodSig, MethodData methodData) {
            String bridgeName = createMethodBridgeName(methodData);
            if (methodData.isStaticMethod()) {
                return methodSig.changeName(bridgeName);
            }
            return methodSig.insertArgumentTypes(0, ca.getOuterClassType())
                    .changeName(bridgeName);
        }

        private String createMethodBridgeName(MethodData methodData) {
            String bridgeName = null;
            if (methodData.isStaticMethod()) {
                bridgeName = methodData.getOwner().getInternalName().replace('/', '_');
            } else {
                bridgeName = ca.getOuterClassType().getInternalName();
                bridgeName = bridgeName.substring(bridgeName.lastIndexOf('/') + 1);
                String ownerName = ca.getOuterClassType().equals(methodData.getOwner()) ? "this" : "super";
                bridgeName += "$" + ownerName;
            }
            return String.format("%s$%s", bridgeName, methodData.getMethodName());
        }

        private void generateBridgeMethod(ClassEmitter ce, FieldSignature fieldSig,
                                          Signature signature) {
            CodeEmitter cm = ce.begin_method(OP_BRIDGE,
                    signature);
            Label l0 = cm.make_label();
            Label l1 = cm.make_label();
            Label l2 = cm.make_label();
            cm.catch_exception(l0, l1, l2, Throwable.class);

            cm.mark(l0);
            cm.getfield(fieldSig.getName());
            cm.load_args();
            cm.invoke_virtual(MethodHandle.class, "invokeExact", signature.getDescriptor());

            cm.mark(l1);
            Label l4 = cm.make_label();
            cm.goTo(l4);

            cm.mark(l2);
            Local thrLocal = cm.store_local(Throwable.class);
            Label l3 = cm.make_label();
            cm.mark(l3);
            cm.new_instance(RuntimeException.class);
            cm.dup();
            cm.load_local(thrLocal);

            cm.invoke_constructor(RuntimeException.class, Throwable.class);
            cm.athrow();

            cm.mark(l4);
            cm.return_value();
            cm.end_method();
        }

        private void specialMethodHandle(ClassEmitter ce, MethodData methodData) {
            CodeEmitter specialMH =
                    ce.begin_method(OP_MH_FACTORY, methodData.getMhValueSig());
            Label l0 = specialMH.make_label();
            Label l1 = specialMH.make_label();
            Label l2 = specialMH.make_label();

            specialMH.catch_exception(l0, l1, l2, NoSuchMethodException.class);
            specialMH.catch_exception(l0, l1, l2, IllegalAccessException.class);

            specialMH.mark(l0);
            specialMH.push(ca.getOuterClassType());
            specialMH.invoke_static(MethodHandles.class, "lookup",
                    MethodType.methodType(MethodHandles.Lookup.class));
            specialMH.invoke_static(MethodHandles.class, "privateLookupIn",
                    MethodType.methodType(MethodHandles.Lookup.class, Class.class, MethodHandles.Lookup.class));
            specialMH.load_args();
            Label l3 = specialMH.make_label();
            specialMH.mark(l3);
            specialMH.invoke_static(MethodType.class, "methodType", MethodType.methodType(MethodType.class,
                    Class.class, Class[].class));
            specialMH.push(ca.getOuterClassType());
            specialMH.invoke_virtual(MethodHandles.Lookup.class, "findSpecial",
                    MethodType.methodType(MethodHandle.class,
                            Class.class, String.class, MethodType.class, Class.class));
            specialMH.mark(l1);
            specialMH.return_value();

            // ex handle
            specialMH.mark(l2);

            Local exLocal = specialMH.store_local(ReflectiveOperationException.class);

            Label l4 = specialMH.make_label();

            specialMH.mark(l4);

            specialMH.new_instance(RuntimeException.class);
            specialMH.dup();
            specialMH.load_local(exLocal);

            specialMH.invoke_constructor(RuntimeException.class, Throwable.class);
            specialMH.athrow();
            specialMH.end_method();
        }

        private void constructorMethodHandle(ClassEmitter ce, MethodData methodData) {
            CodeEmitter ctorMH =
                    ce.begin_method(OP_MH_FACTORY, methodData.getMhValueSig());
            Label l0 = ctorMH.make_label();
            Label l1 = ctorMH.make_label();
            Label l2 = ctorMH.make_label();

            ctorMH.catch_exception(l0, l1, l2, NoSuchMethodException.class);
            ctorMH.catch_exception(l0, l1, l2, IllegalAccessException.class);

            ctorMH.mark(l0);
            ctorMH.push(ca.getOuterClassType());
            ctorMH.invoke_static(MethodHandles.class, "lookup",
                    MethodType.methodType(MethodHandles.Lookup.class));
            ctorMH.invoke_static(MethodHandles.class, "privateLookupIn",
                    MethodType.methodType(MethodHandles.Lookup.class, Class.class, MethodHandles.Lookup.class));
            ctorMH.load_args();
            Label l3 = ctorMH.make_label();
            ctorMH.mark(l3);
            ctorMH.invoke_static(MethodType.class, "methodType", MethodType.methodType(MethodType.class,
                    Class.class, Class[].class));
            ctorMH.invoke_virtual(MethodHandles.Lookup.class, "findConstructor",
                    MethodType.methodType(MethodHandle.class,
                            Class.class, MethodType.class));
            ctorMH.mark(l1);
            ctorMH.return_value();

            // ex handle
            ctorMH.mark(l2);

            Local exLocal = ctorMH.store_local(ReflectiveOperationException.class);

            Label l4 = ctorMH.make_label();

            ctorMH.mark(l4);

            ctorMH.new_instance(RuntimeException.class);
            ctorMH.dup();
            ctorMH.load_local(exLocal);

            ctorMH.invoke_constructor(RuntimeException.class, Throwable.class);
            ctorMH.athrow();
            ctorMH.end_method();
        }

        private void virtualMethodHandle(ClassEmitter ce, MethodData methodData) {
            CodeEmitter virtualMH =
                    ce.begin_method(OP_MH_FACTORY, methodData.getMhValueSig());
            Label l0 = virtualMH.make_label();
            Label l1 = virtualMH.make_label();
            Label l2 = virtualMH.make_label();

            virtualMH.catch_exception(l0, l1, l2, NoSuchMethodException.class);
            virtualMH.catch_exception(l0, l1, l2, IllegalAccessException.class);

            virtualMH.mark(l0);
            virtualMH.push(ca.getOuterClassType());
            virtualMH.invoke_static(MethodHandles.class, "lookup",
                    MethodType.methodType(MethodHandles.Lookup.class));
            virtualMH.invoke_static(MethodHandles.class, "privateLookupIn",
                    MethodType.methodType(MethodHandles.Lookup.class, Class.class, MethodHandles.Lookup.class));
            virtualMH.load_args();
            Label l3 = virtualMH.make_label();
            virtualMH.mark(l3);
            virtualMH.invoke_static(MethodType.class, "methodType", MethodType.methodType(MethodType.class,
                    Class.class, Class[].class));
            virtualMH.invoke_virtual(MethodHandles.Lookup.class, "findVirtual",
                    MethodType.methodType(MethodHandle.class,
                            Class.class, String.class, MethodType.class));
            virtualMH.mark(l1);
            virtualMH.return_value();

            // ex handle
            virtualMH.mark(l2);

            Local exLocal = virtualMH.store_local(ReflectiveOperationException.class);

            Label l4 = virtualMH.make_label();

            virtualMH.mark(l4);

            virtualMH.new_instance(RuntimeException.class);
            virtualMH.dup();
            virtualMH.load_local(exLocal);

            virtualMH.invoke_constructor(RuntimeException.class, Throwable.class);
            virtualMH.athrow();
            virtualMH.end_method();
        }

        private void staticMethodHandle(ClassEmitter ce, MethodData methodData) {
            CodeEmitter staticMH =
                    ce.begin_method(OP_MH_FACTORY, methodData.getMhValueSig());
            Label l0 = staticMH.make_label();
            Label l1 = staticMH.make_label();
            Label l2 = staticMH.make_label();

            staticMH.catch_exception(l0, l1, l2, NoSuchMethodException.class);
            staticMH.catch_exception(l0, l1, l2, IllegalAccessException.class);

            staticMH.mark(l0);
            staticMH.push(ca.getOuterClassType());
            staticMH.invoke_static(MethodHandles.class, "lookup",
                    MethodType.methodType(MethodHandles.Lookup.class));
            staticMH.invoke_static(MethodHandles.class, "privateLookupIn",
                    MethodType.methodType(MethodHandles.Lookup.class, Class.class, MethodHandles.Lookup.class));
            staticMH.load_args();
            Label l3 = staticMH.make_label();
            staticMH.mark(l3);
            staticMH.invoke_static(MethodType.class, "methodType", MethodType.methodType(MethodType.class,
                    Class.class, Class[].class));
            staticMH.invoke_virtual(MethodHandles.Lookup.class, "findStatic",
                    MethodType.methodType(MethodHandle.class,
                            Class.class, String.class, MethodType.class));
            staticMH.mark(l1);
            staticMH.return_value();

            // ex handle
            staticMH.mark(l2);

            Local exLocal = staticMH.store_local(ReflectiveOperationException.class);

            Label l4 = staticMH.make_label();

            staticMH.mark(l4);

            staticMH.new_instance(RuntimeException.class);
            staticMH.dup();
            staticMH.load_local(exLocal);

            staticMH.invoke_constructor(RuntimeException.class, Throwable.class);
            staticMH.athrow();
            staticMH.end_method();
        }

        private void setMethodMHValue(ClassEmitter ce, FieldSignature fieldSig, MethodData methodData) {
            if (!ca.existMethodMH(methodData)) {
                switch (methodData.getOpcode()) {
                    case ASMOpcodes.INVOKESTATIC:
                        staticMethodHandle(ce, methodData);
                        break;
                    case ASMOpcodes.INVOKESPECIAL:
                        if (methodData.isConstructor()) {
                            constructorMethodHandle(ce, methodData);
                        } else {
                            specialMethodHandle(ce, methodData);
                        }
                        break;
                    case ASMOpcodes.INVOKEINTERFACE:
                    case ASMOpcodes.INVOKEVIRTUAL:
                        virtualMethodHandle(ce, methodData);
                }
                ca.addMethodMH(methodData);
            }
            CodeEmitter staticBlock = ce.getStaticBlock();

            staticBlock.push(methodData.getOwner());
            if (!methodData.isConstructor()) {
                staticBlock.push(methodData.getMethodName());
            }

            staticBlock.push(methodData.getReturnType());

            Type[] argTypes = methodData.getArgumentTypes();
            staticBlock.push(argTypes.length);
            staticBlock.newarray(Class.class);
            for (int i = 0; i < argTypes.length; i++) {
                staticBlock.dup();
                staticBlock.push(i);
                staticBlock.push(argTypes[i]);
                staticBlock.aastore();
            }

            staticBlock.invoke_static_this(methodData.getMhValueSig());
            staticBlock.putfield(fieldSig.getName());
        }

        @Override
        public void visitLocalVariable(String name, String descriptor, String signature, Label start, Label end,
                                       int index) {
            if (index == 0 && !TypeUtils.isStatic(oldAccess)) {
                name = String.format(DEFAULT_OUTER_THIS, getSimpleName(descriptor));
            }
            super.visitLocalVariable(name, descriptor, signature, start, end, index);
        }

        private static String getSimpleName(String typeDescriptor) {
            String internalName = Type.getType(typeDescriptor).getInternalName();
            return internalName.substring(internalName.lastIndexOf("/") + 1);
        }
    }

    private static class FieldData {
        protected final int opcode;

        protected final Type owner;

        protected final String name;

        protected final Type type;

        protected final String mhMethodInvokeName;

        public FieldData(int opcode, Type owner, String name, Type type, String mhName) {
            this.opcode = opcode;
            this.owner = owner;
            this.name = name;
            this.type = type;
            this.mhMethodInvokeName = mhName;
        }

        public static FieldData of(int opcode, Type owner, String name, Type type) {
            String mhMethodInvokeName = null;
            switch (opcode) {
                case ASMOpcodes.GETFIELD:
                    mhMethodInvokeName = "findGetter";
                    break;
                case ASMOpcodes.PUTFIELD:
                    mhMethodInvokeName = "findSetter";
                    break;
                case ASMOpcodes.GETSTATIC:
                    mhMethodInvokeName = "findStaticGetter";
                    break;
                case ASMOpcodes.PUTSTATIC:
                    mhMethodInvokeName = "findStaticSetter";
                    break;
                default:
                    throw new IllegalArgumentException("opcode: " + opcode);
            }
            return new FieldData(opcode, owner, name, type, mhMethodInvokeName);
        }

        public int getOpcode() {
            return opcode;
        }

        public Type getOwner() {
            return owner;
        }

        public String getName() {
            return name;
        }

        public Type getType() {
            return type;
        }

        public String getMhMethodInvokeName() {
            return mhMethodInvokeName;
        }

        public boolean isStaticField() {
            return ASMOpcodes.PUTSTATIC == opcode || ASMOpcodes.GETSTATIC == opcode;
        }

        @Override
        public final boolean equals(Object other) {
            if (this == other) return true;
            return other instanceof FieldData && equals((FieldData) other);
        }

        public boolean equals(FieldData other) {
            return opcode == other.opcode && Objects.equals(owner, other.owner) && Objects.equals(name,
                    other.name) && Objects.equals(type, other.type) && Objects.equals(mhMethodInvokeName,
                    other.mhMethodInvokeName);
        }

        @Override
        public int hashCode() {
            return Objects.hash(opcode, owner, name, type);
        }

        public static boolean isStaticField(int opcode) {
            return ASMOpcodes.PUTSTATIC == opcode || ASMOpcodes.GETSTATIC == opcode;
        }
    }

    public static class MethodData {
        private static final String MH_METHOD_VALUE_PRE = "EASY_AOP$$";

        protected final int opcode;

        protected final Type owner;

        protected final Signature methodSig;

        protected Signature mhSig;

        protected Signature mhValueSig;

        protected MethodData(int opcode, Type owner, Signature methodSig, Signature mhSig, Signature mhValueSig) {
            this.opcode = opcode;
            this.owner = owner;
            this.methodSig = methodSig;
            this.mhSig = mhSig;
            this.mhValueSig = mhValueSig;
        }

        public static MethodData of(int opcode, Type owner, Signature methodSig) {
            Signature sig = null;
            MethodType mhValueMT = null;
            switch (opcode) {
                case ASMOpcodes.INVOKESTATIC:
                    sig = new Signature("findStatic", MethodType.methodType(MethodHandle.class, Class.class,
                            String.class,
                            MethodType.class));
                    mhValueMT = MethodType.methodType(MethodHandle.class, Class.class, String.class, Class.class,
                            Class[].class);
                    break;
                case ASMOpcodes.INVOKESPECIAL:
                    if (Constants.NAME_CTOR.equals(methodSig.getName())) {
                        sig = new Signature("findConstructor", MethodType.methodType(MethodHandle.class, Class.class,
                                MethodType.class));
                        mhValueMT = MethodType.methodType(MethodHandle.class, Class.class, Class.class,
                                Class[].class);
                    } else {
                        sig = new Signature("findSpecial", MethodType.methodType(MethodHandle.class, Class.class,
                                String.class,
                                MethodType.class, Class.class));
                        mhValueMT = MethodType.methodType(MethodHandle.class, Class.class, String.class, Class.class,
                                Class[].class);
                    }
                    break;
                case ASMOpcodes.INVOKEINTERFACE:
                case ASMOpcodes.INVOKEVIRTUAL:
                    sig = new Signature("findVirtual", MethodType.methodType(MethodHandle.class, Class.class,
                            String.class,
                            MethodType.class));
                    mhValueMT = MethodType.methodType(MethodHandle.class, Class.class, String.class, Class.class,
                            Class[].class);
                    break;
                default:
                    throw new IllegalArgumentException("opcode: " + opcode);
            }
            Signature mhValueSig = new Signature(MH_METHOD_VALUE_PRE + sig.getName(), mhValueMT);
            return new MethodData(opcode, owner, methodSig, sig, mhValueSig);
        }

        public int getOpcode() {
            return opcode;
        }

        public Type getOwner() {
            return owner;
        }

        public String getMethodName() {
            return methodSig.getName();
        }

        public String getMethodDescriptor() {
            return methodSig.getDescriptor();
        }

        public Type getReturnType() {
            return methodSig.getReturnType();
        }

        public Type[] getArgumentTypes() {
            return methodSig.getArgumentTypes();
        }

        public Signature getMethodSig() {
            return methodSig;
        }

        public Signature getMhSig() {
            return mhSig;
        }

        public Signature getMhValueSig() {
            return mhValueSig;
        }

        public boolean isConstructor() {
            return Constants.NAME_CTOR.equals(getMethodName());
        }

        public boolean isStaticMethod() {
            return ASMOpcodes.INVOKESTATIC == opcode;
        }

        @Override
        public final boolean equals(Object obj) {
            return this == obj || obj instanceof MethodData && equals((MethodData) obj);
        }

        public boolean equals(MethodData other) {
            return Objects.equals(owner, other.owner) && Objects.equals(methodSig,
                    other.methodSig);
        }

        @Override
        public int hashCode() {
            return Objects.hash(owner, methodSig);
        }

        public static boolean isStaticMethod(int opcode) {
            return ASMOpcodes.INVOKESTATIC == opcode;
        }
    }
}